错误和异常处理
错误和异常处理
异常类型
Python 有两种错误很容易辨认:
-
语法错误(SyntaxError),编译时/解析时的错误,比如 if 语句最后少了一个
:
这种File "E:\PythonProject\PythonLearn\errors\error_test.py", line 7 if a < 10 ^ SyntaxError: expected ':'
-
异常(Error),运行时错误。Python 程序的语法是正确的,在运行它的时候,也有可能发生错误。运行期检测到的错误被称为异常,比如 字符串类型的变量直接
+
一个整形的变量,Traceback (most recent call last): File "E:\PythonProject\PythonLearn\errors\error_test.py", line 10, in <module> print(f"{a+'aaa'}") TypeError: unsupported operand type(s) for +: 'int' and 'str'
异常处理
try/except - 异常捕获

关于这个异常对象的属性和方法,参考 内置异常 — Python 3.10.12 文档
简单实践如下:
try:
print(f"{12 / 0}")
print("it's ok")
# 一个 try 语句可能包含多个 except 子句,分别来处理不同的特定的异常。最多只有一个分支会被执行
except ValueError as error:
print(f"输出 {error}")
# 使用 raise 语句直接将捕获的异常抛出,甚至都不需要异常的变量名
raise
except ZeroDivisionError as error:
print("error occurred")
print(f"输出 {error}")
# 你可以把多个异常写到一起,放到一个元组里
except (RuntimeError, TypeError, NameError) as error:
print(f"输出 {error}")
# 可以捕获所有的异常,这个一般放在最后,这样可以用于捕获任意异常
except Exception as e:
print(e)
# 上面的 except 也可以省略异常的类型
except:
print("uncaught exception,just raise it")
# 使用 raise 语句直接将捕获的异常抛出,甚至都不需要异常的变量名
raise
输出
error occurred
输出 division by zero
else 子句
try/except 语句还有一个可选的 else 子句,else 子句,必须放在所有的 except 子句之后。如果没有 except 子句,那也就不能有 else 子句。
else 子句将在 try 子句没有发生任何异常的时候执行。有异常的时候会走 except 之后直接走 finally(如果有的话)。

使用 else 子句最大的好处是让我们可以不把所有的语句放到 try 子句中(说的就是你,Java!),这样可以避免一些意想不到,而 except 又无法捕获的异常。
else 子句是 Java 中没有的
简单实践如下:
try:
print(f"{12 / 2}")
print("it's ok")
except ZeroDivisionError as error:
print("error occurred")
print(f"输出 {error}")
except:
print("uncaught exception,just raise it")
# 使用 raise 语句直接将捕获的异常抛出,甚至都不需要异常的变量名
raise
else:
print("it's alright")
输出
6.0
it's ok
it's alright
当然,你也可以在 else 子句中再次使用 try/except
finally 子句 - 清理行为
finally 子句无论异常是否发生都会执行

简单实践如下:
try:
print(f"{12 / 0}")
print("it's ok")
except ZeroDivisionError as error:
print("error occurred")
print(f"输出 {error}")
except:
print("uncaught exception,just raise it")
# 使用 raise 语句直接将捕获的异常抛出,甚至都不需要异常的变量名
raise
else:
print("it's alright")
finally:
print("finally action : calculate over ")
输出
error occurred
输出 division by zero
finally action : calculate over
注意,如果一个异常在 try 子句里(或者在 except 和 else 子句里)被抛出,而又没有任何的 except 把它截住,那么这个异常会在 finally 子句执行后被抛出。
这个跟 Java 其实是一样的逻辑:
报错,被捕捉到,执行 finally
报错,没捕捉到,执行 finally,然后抛出异常
异常要么被捕捉到,要么继续向上抛出,没有第三种可能
try:
print(f"{12 / 0}")
print("it's ok")
finally:
print("finally action : calculate over ")
return 语句
我们假设一个场景,一个方法中,包含一个 try-except-else-finally 代码块,这个代码块中,try 子句、except 子句、else 子句、finally 子句最后都有 return 语句,同时 try-except-else-finally 后面也有方法语句,在方法的最后也有 return 语句,总共 5 个 return 语句,那调用这个方法,最终返回的,是哪个 return 的值呢?
-
try 子句报错,try 子句的 return 和 else 的 retrun 不生效,except 和 finally 中的 return 语句生效,此时,finally 在 except 后面执行,所以,如果两者都有 return 语句,最终生效的是 finally 中的 return,如果 finally 中没有 return,except 中的 return 生效,
而且 except 和 finally 中任意一个字句的 return 语句生效后,方法直接返回,try-except-else-finally 后面的代码不会执行
-
try 子句不报错,except 子句失效,try 子句、else 子句和 finally 中的 return 语句会生效(不一定同时),如果 try 中包含 return 语句,那么代码执行顺序是 try->finally,因为 try 中包含了 return,else 被直接跳过,但是 finally 必须执行,同时,如果 finally 中有 return 语句,最终返回的将是 finally 的 return 语句,如果 try 中不包含 return 语句,那么代码执行顺序是 try->else->finally,同样的,else 的 return 语句执行之后,也必须去执行 finally,同时,如果 finally 中有 return 语句,最终返回的将是 finally 的 return 语句。
跟前面一样,try 子句、else 子句和 finally 中任意一个字句的 return 语句生效后,方法直接返回,try-except-else-finally 后面的代码不会执行。
总的来说,规律就是:
方法内部只要执行了 return 语句,都会直接跳出当前方法,后面还有 return 语句也会被忽略,但是如果当前 return 语句的后面有 finally 子句,那在执行当前 return 语句之后,并不是直接返回,而是还要去执行完 finally 子句才能返回,此时,如果 finally 代码块中的代码会影响 return 表达式的值,最终返回的结果跟执行 finally 代码块之前一样,不会改变,就好像已经固化了一样,而如果 finally 子句也有 return,那就会以 finally 子句的 renturn 做为最终结果返回,这个有点像返回值的覆盖。
通过断点就看得很清楚,执行完 try 的 return 语句之后,下一步,直接跳过了 else 语句,而直接到了 finally 子句。
经过严谨测试,以上规律,在 JavaScript 和 Java 中的也是一样的,
简单实践如下:
# 通过注释 try-except-else-finally 中各个子句的 return 语句来看看最终生效的是哪个 return
def try_return():
print("try_return")
return 11
def test_return_in_excetp():
try:
print(f"{12 / 0}")
print("it's ok")
# return try_return()
except ZeroDivisionError as error:
print("error occurred")
print(f"输出 {error}")
return 12
except:
print("uncaught exception,just raise it")
# 使用 raise 语句直接将捕获的异常抛出,甚至都不需要异常的变量名
raise
else:
print("it's alright")
return 13
finally:
print("finally action : calculate over ")
return 14
print("function run")
return 20
print(test_return_in_excetp())
输出
error occurred
输出 division by zero
finally action : calculate over
14
finally 不会修改 return 语句的值
print("---- finally 不会修改 return 语句的值 ----")
value = 10
def try_return():
print("try_return")
return value
def test_return_in_excetp():
try:
# print(f"{12 / 0}")
print("it's ok")
return try_return()
except ZeroDivisionError as error:
print("error occurred")
print(f"输出 {error}")
return 12
except:
print("uncaught exception,just raise it")
# 使用 raise 语句直接将捕获的异常抛出,甚至都不需要异常的变量名
raise
else:
print("it's alright")
return 13
finally:
print("finally action : calculate over ")
value = 11
# return 14
print("function run")
return 20
print(test_return_in_excetp())
输出
it's ok
try_return
finally action : calculate over
10
预定义的 finally - with 关键字
大神博客:Python3: 异常处理 与 with 语句_python with 捕获异常_谢 TS 的博客-CSDN 博客
finally 代码块可以看作是一种清理行为,即,不管代码报不报错,finally 都要执行,这里就不得不提到 with 关键字
Python 中的 with 语句用于异常处理,封装了 try-finally
编码范式,提高了易用性。with 语句使代码更清晰、更具可读性,它简化了文件流等公共资源的管理。在处理文件对象时使用 with 关键字是一种很好的做法。
with 语句实现原理建立在上下文管理器(可以将其理解为一个接口)之上。上下文管理器是一个实现 __enter__
和 __exit__
方法的类。
with obj as f:
# f.method(...)
pass
-
obj 表示一个对象(或是一个表达式,结果为一个对象)
-
调用 obj 对象的
__enter__
方法,返回值赋值给 as 右边的变量 f,
即:f = obj.__enter__
()
-
执行 with 代码块中的代码
-
执行完 with 代码块中的代码后,无论是否发生异常,调用 obj 的
__exit__
方法,
即:obj.__exit__
(...)
上面代码相当于:
obj = ...
f = obj.__enter__()
try:
# f.method(...)
pass
finally:
obj.__exit__(...)
as 也可以省略,此时 __enter__
方法可以不返回对象
with obj:
# obj.method(...)
pass
文件对象实现上下文管理器
在文件对象中定义了 __enter__
和 __exit__
方法,即文件对象也实现了上下文管理器,首先调用 __enter__
方法,然后执行 with 语句中的代码,最后调用 __exit__
方法。即使出现错误,也会调用 __exit__
方法,也就是会关闭文件流。
在 typing.py
的 IO
类中可以看到这两个方法的定义:
@abstractmethod
def __enter__(self) -> 'IO[AnyStr]':
pass
@abstractmethod
def __exit__(self, type, value, traceback) -> None:
pass
其中 __exit__
的实现确实时 close 方法
self.stream.close()
自定义类实现上下文管理器
当然,你在自定义类的时候,也可以实现上下文管理器,然后对这个类型的对象使用 with as 语法。
在实现了上下文管理器的自定义类中,我们一般会提供三个方法,
-
__enter__
返回上下文管理对象,注意,这个方法需要将对象 return 出去,这个对象将赋值给 with as 语句中 as 后面的那个变量,通常返回自己,此时是有可能会报错的,比如我们返回文件对象,文件有可能不存在,因此,我们最好在获取对象的方法包上 try-except,处理一下可能会出现的异常,但是要注意,即使出现异常,依然要返回一个默认的文件对象,否则with as
语句会报错,表示'NoneType' object has no attribute 'xxx'
(xxx 就是工作方法)当
with as
省略as
的时候,此时__enter__
方法可以不返回对象 -
业务方法,这个方法一般会在
with as
的方法体中调用,也可能会报错,我们可以在with as
的方法体对其包一层 try-except,也可以在自定义类中定义此方法的时候自己包一层 try-except。 -
__exit__
,相当于 finally 子句,我们可以在这里执行清理工作,这个方法提供了几个参数,根据这几个参数,我们可以针对特定类型进行特定处理,比如日志记录一下,我们没有办法在这里取消异常的抛出,也就是人们常说的吃掉这个异常-
exc_type 异常类型
-
exc_val 异常信息
-
exc_tb 异常栈对象
-
注意,其实这个时候,with as
后面处理的对象,不一定是自定义类的实例,虽然通常是,但是可以不是,其可以是一个别的变量。
简单实践如下:
class test_keyword_with():
def do_work(self):
print("doing work")
print(12 / 0)
def __enter__(self):
print("enter obj")
# 重点,需要返回当前对象,这个对象将赋值给 with as 语句中 as 后面的那个变量
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print("exit obj")
# 通过 exc_type(异常类型), (exc_val)异常信息 我们可以处理异常信息;exc_tb 异常栈对象
# 类型比较,直接比较即可
if exc_type is not None:
# 可以针对特定类型进行处理,比如日志输出
# 注意,我们没有办法在这里取消异常的抛出,也就是人们常说的吃掉这个异常
print(exc_val)
# with test_keyword_with() as with_obj:
# with_obj.do_work()
# as 也可以省略,此时 __enter__ 方法可以不返回对象
with_obj = test_keyword_with()
with with_obj:
# 在这里进行了处理,__exit__ 方法中就不会有异常信息了
try:
with_obj.do_work()
except ZeroDivisionError as e:
print("exception occur",e)
输出
enter obj
doing work
exception occur division by zero
exit obj
@contextmanager 装饰器 - 相当好用
Python 中的装饰器和 Java 中的注解的异同点和讨论
浅谈 java 中注解和 python 中装饰器的区别_装饰器和注解的区别_BeanInJ 的博客-CSDN 博客
https://www.zhihu.com/question/345262158/answer/828687881
具体请看《装饰器.md》
之前我们想要实现上下文管理器配合 with as
使用,得自定义一个类,但是大部分的时候,我只是想对一个已经存在的对象使用上下文管理器,有没有简单的办法呢?
@contextmanager
装饰器更适用于对已存在的对象比如第三方库中的对象添加上下文管理器,自定义类实现上下文管理器的适用于自己新建的类。
有,那就是可以使用 @contextmanager
装饰器来修饰我们获取这个对象的方法,而且在 @contextmanager
装饰器装饰的方法中,写法更加灵活
@contextmanager
def some_generator(<arguments>):
<setup>
try:
yield <value>
finally:
<cleanup>
关于 yield 关键字,请看《迭代器.md》中的
生成器
小节
@contextmanager
装饰器已经帮我们处理好了 __enter__
和 __exit__
方法:
-
yield 之前的代码相当于
__enter__
-
yield 之后的代码相当于
__exit__
-
yield 指定的对象即为执行 with 语句赋值给 as 右边变量的对象
然后就可以直接在 with as
中使用这个方法,所以本质上,@contextmanager
装饰器是提供了一种更加灵活的实现上下文管理器的方式
with some_generator(<arguments>) as <variable>:
<body>
最终的效果其实就等同于:
<setup>
try:
<variable> = <value>
<body>
finally:
<cleanup>
通过 @contextmanager
装饰器,我们把获取对象和之后对这个对象的清理跟获取这个对象之后的操作完全隔开了,方便我们对代码的管理。
简单实践如下:下面演示了通过上下文管理器获取一个文件对象,然后读取文件的过程。
# @contextmanager 装饰器
from contextlib import contextmanager # 导入上下文管理器
# @contextmanager
@contextmanager
def get_file_obj():
# 创建一个空文件
empty_file_name = "./emptyfile.txt"
empty_file_obj = open(empty_file_name, "w")
empty_file_obj.close()
empty_file_obj = open(empty_file_name, "r")
file_obj = None
try:
# 文件不存在
file_obj = open("./test.txt", "r", encoding="utf-8")
yield file_obj
except Exception as error:
# 可以捕捉到异常并阻止异常的抛出,但是此时我们仍然需要返回空的文件对象给 with as 语句
print("文件不存在")
yield empty_file_obj
finally:
empty_file_obj.close()
# 删除临时文件
os.remove(empty_file_name)
if file_obj != None:
file_obj.close()
with get_file_obj() as file_obj:
print(file_obj.readlines())
输出
文件不存在
[]
输出异常到文件中
# 输出错误信息到文件中
import traceback
try:
print(f"{12 / 0}")
except ZeroDivisionError as error:
print("error occurred")
# print_exc 不指定输出文件对象的话,默认输出到 sys.stderr 文件对象
traceback.print_exc(file=open("./error.txt", "a"))
抛出异常 - raise
BaseException 是所有异常的公共基类,Exception 继承自 BaseException,Exception
是所有非退出异常的公共基类,其他非退出异常继承自 Exception,raise
只能抛出 BaseException
异常类型(包括其子类)或 异常类型的实例,注意,可以抛出一个 异常的类型 或 异常类型的实例
我们使用 raise 语句抛出一个指定的异常。raise 后面的类型必须是一个异常的实例或者是异常的类(也就是 Exception 的子类)
类似于 Java 的
Throw
关键字,但是 Java 可没办法直接排除一个类,Python 应该是做了额外的处理
简单实践如下:
# 手动抛出异常
b = 10
if b == 10:
# 手动抛出 ArithmeticError 异常,自定义异常提示信息
raise ArithmeticError(f"b 的值不对:{b}")
# 或者直接抛出类
# raise ZeroDivisionError
pass
Python 内置的常见的异常类型
常见的异常有
-
FileNotFoundError:文件不存在的异常
-
ZeroDivisionError:出书为 0 的异常
-
NameError:变量未定义
-
TypeError:操作符两端的变量的类型不符合要求,比如字符串类型的变量直接
+
一个整形的变量 -
ArithmeticError:四则运算异常
用户自定义异常
你可以通过创建一个新的异常类来拥有自己的异常。异常类继承自 Exception 类,可以直接继承,或者间接继承,大多数的异常的名字都以 "Error" 结尾,就跟标准的异常命名一样。
简单实践如下:
# 自定义异常
class MyError(Exception):
def __init__(self, message):
self.message = message
def __str__(self):
return self.message
raise MyError("自定义错误类型")
异常的传递性
异常是具有传递性的,举个例子,main 方法调用 func01,func01 调用 func02,当函数 func02 中发生异常,并且没有捕获处理这个异常的时候,异常会传递到函数 func01, 当 func01 也没有捕获处理这个异常的时候,main 函数会捕获这个异常, 这就是异常的传递性,而当所有函数都没有捕获异常的时候,程序就会报错。
因此,我们捕获异常不需要把 try-except 语句写在实际报错的那一行,写在发起函数调用的地方也是可以捕获到异常的